"""
Student/Course/Advisor resources

This demo illustrates how to provide/deny access to resources
based on request context.

Actor Roles (the same person/actor may have several roles):
    students, instructors, advisors

Access Rules:
    Students should have read-only access to their own grades.
    Instructors whould have read/write access to grades for their classes.
    Advisors should have read-only access to their advisees grades.

For simplicity the INTERNAL resource data structure
is implemented in-memory using lists and dictionaries
GLOBAL_INFO shown below.

For this reason this resource implementation will not
work correctly for updates under a multi-process server.

EXTERNAL RESOURCE PATHS

Top level:
    ["grades"]
    -- accessible only if the request is validated as representing a known user.
    -- returns a list of valid "roles" always ["advisor", "instructor", "student"]

Paths for the advisor role:
    ["grades", "advisor"]
    -- if authenticated as "Ann" returns the list of Ann's advisees ["Bob", "Jim"]
    ["grades", "advisor", "Jim"]
    -- if authenticated as "Ann" returns the list of Jims courses ["Phy300"]
    ["grades", "advisor", "Jim", "Phy300"]
    -- if authenticated as "Ann" returns the read only grade for Jim in Phy300: "C".

Paths for the instructor role
    ["grades", "instructor"]
    -- if authenticated as "Ann" returns the list of courses Ann teaches: ["Bio101"]
    ["grades", "instructor", "Bio101"]
    -- if authenticated as "Ann" returns the list of students in Bio101: ["Sue", "Bob"]
    ["grades", "instructor", "Bio101", "Bob"]
    -- if authenticated as "Ann" returns the read/write grade for Bob in Bio101: "B".

Paths for the student role:
    ["grades", "student"]
    -- if authenticated as "Bob" returns the list of courses bob attends: ["Bio101", "Phy300"]
    ["grades", "student", "Phy300"]
    -- if authenticated as "Bob" returns the read only grade for Bob in Phy300: "C".
"""

from whiff import gateway
from whiff import whiffenv
from whiff.resources import AuthorizedResource

# "in core" database of users, courses, grades, advisors, instructors
GLOBAL_INFO = {
    "Ann": {
        "password": "annspassword",
        "teaches": ["Bio101"],
        "advises": ["Bob", "Jim"],
        "courseGrades": {}
        },
    "Ted": {
        "password": "tedspassword",
        "teaches": ["Phy300"],
        "advises": ["Sue", "Nel"],
        "courseGrades": {}
        },
    "Sue": {
        "password": "suespassword",
        "teaches": [],
        "advises": [],
        "courseGrades": {"Bio101": "A"}
        },
    "Bob": {
        "password": "bobspassword",
        "teaches": [],
        "advises": [],
        "courseGrades": {"Bio101": "B", "Phy200": "C"}
        },
    "Jim": {
        "password": "jimspassword",
        "teaches": [],
        "advises": [],
        "courseGrades": {"Phy300":"D"}
        },
    "Nel": {
        "password": "nelspassword",
        "teaches": [],
        "advises": [],
        "courseGrades": {"Phy300": "B"}
        },
    }

def Finder():
    "find a resource relating to students, courses, grades, advisors, instructors."
    result = AuthorizedResource.BranchFinder(userGuard)

    # student resource branch allows a student to examine his own grades
    result.branch("student", StudentLocation, studentGuard)

    # instructor resource branch allows an instructor to examine his courses and modify grades therein.
    result.branch("instructor", InstructorLocation, instructorGuard)

    # advisor resource allows advisor to view courses and grades for advisees
    result.branch("advisor", AdvisorLocation, advisorGuard)
    return result

# Guards:
#
# Examine the environment and certificates and
# return possibly modified certificates when access is permitted
# or return None if access is not permitted.

def userGuard(env, certificates):
    "validate the request as representing a known user"
    result = certificates.copy()
    userid = whiffenv.cgiGet(env, "userid")
    password = whiffenv.cgiGet(env, "password")
    #pr "got (userid, password)", (userid, password)
    if not userid or not password:
        # not logged in: access denied
        return None
    userInfo = GLOBAL_INFO.get(userid)
    if not userInfo:
        # no such user: access denied
        #pr "no such user"
        return None
    # check password
    if password!=userInfo.get("password"):
        # invalid log in: access denied
        #pr "invalid log in", userInfo.get("password")
        return None
    result["user"] = userid
    return result

def studentGuard(env, certificates):
    "validate the request as a student"
    # no checking required: if not taking any courses the resource subtree will be empty.
    result = certificates.copy()
    result["student"] = certificates["user"]
    return result

def advisorGuard(env, certificates):
    "validate the request as a advisor"
    # no checking required: if no advisees the resource subtree will be empty.
    result = certificates.copy()
    result["advisor"] = certificates["user"]
    return result

def instructorGuard(env, certificates):
    "validate the request as a instructor"
    # no checking required: if not teaching any course the resource subtree will be empty
    result = certificates.copy()
    result["instructor"] = certificates["user"]
    return result

def studentGradeGuard(env, certificates, courseName):
    "validate the request as able to access a course grade for the current student"
    # grade is available if certified student is in the course
    studentName = certificates["student"]
    return gradeForStudentInCourseGuard(certificates, studentName, courseName)

def courseGradeGuard(env, certificates, studentName):
    "allow access to student course grade if the student is in the current course"
    courseName = certificates["course"]
    return gradeForStudentInCourseGuard(certificates, studentName, courseName)

def gradeForStudentInCourseGuard(certificates, studentName, courseName):
    "check that student is in course and if so allow access"
    studentInfo = GLOBAL_INFO[studentName]
    courseGrades = studentInfo["courseGrades"]
    if courseName in courseGrades:
        result = certificates.copy()
        result["course"] = courseName
        result["student"] = studentName
        return result
    else:
        # student is not in course: access denied
        return None

def instructorCourseGuard(env, certificates, courseName):
    "validate assertion: certified instructor teaches the course"
    #pr "instructorCourseGuard", (courseName, certificates)
    instructorName = certificates["instructor"]
    instructorInfo = GLOBAL_INFO[instructorName]
    #pr "instructor info", (instructorName, instructorInfo)
    if courseName in instructorInfo["teaches"]:
        # certified instructor teaches the course: permit access
        result = certificates.copy()
        result["course"] = courseName
        # permit writes to grades in this course
        result["gradeWriteable"] = True
        return result
    else:
        # instructor does not teach course: deny access
        #pr "instructor does not teach", (instructorName, courseName)
        return None

def advisorStudentGuard(env, certificates, studentName):
    "validate assertion: certified instructor teaches the course"
    advisorName = certificates["advisor"]
    advisorInfo = GLOBAL_INFO[advisorName]
    if studentName in advisorInfo["advises"]:
        # certified advisor advises this student: permit access
        result = certificates.copy()
        result["student"] = studentName
        # UNCOMMENT to permit writes to grades for advisees
        #result["gradeWriteable"] = True
        return result
    else:
        # instructor does not teach course: deny access
        return None

# Locations:
#
# Find objects corresponding to paths.
# Get or put local values when appropriate for empty paths.
# Locate descendent locations for nonempty paths, if appropriate.
# Consult guard functions to validate access permission.

class StudentLocation(AuthorizedResource.Location):
    """
    Student location gives access to courses attended by current student.
    """
    def __init__(self, env, certificates):
        self.setContext(env, certificates)
    def getHere(self):
        "return the list of courses for this student"
        studentName = self.certificates["student"]
        studentInfo = GLOBAL_INFO[studentName]
        courseGrades = studentInfo["courseGrades"]
        result = courseGrades.keys()
        result.sort()
        return result
    def locate(self, courseName):
        "return location for course grade if student in course"
        certificates = studentGradeGuard(self.env, self.certificates, courseName)
        if certificates:
            return GradeLocation(self.env, certificates)
        else:
            raise gateway.AccessDenied, "student is not in course"

class InstructorLocation(AuthorizedResource.Location):
    """
    Instructor location gives access to courses taught by current instructor.
    """
    def __init__(self, env, certificates):
        self.setContext(env, certificates)
    def getHere(self):
        "return list of courses for this instructor"
        instructorName = self.certificates["instructor"]
        instructorInfo = GLOBAL_INFO[instructorName]
        return instructorInfo["teaches"]
    def locate(self, courseName):
        "return course location if instructor teaches course"
        certificates = instructorCourseGuard(self.env, self.certificates, courseName)
        if certificates:
            return CourseLocation(self.env, certificates)
        else:
            raise gateway.AccessDenied, "course access is not permitted"

class AdvisorLocation(AuthorizedResource.Location):
    """
    Advisor location gives access to students advised by current advisor.
    """
    def __init__(self, env, certificates):
        self.setContext(env, certificates)
    def getHere(self):
        "return list of student names for advisor"
        advisorName = self.certificates["advisor"]
        advisorInfo = GLOBAL_INFO[advisorName]
        return advisorInfo["advises"]
    def locate(self, studentName):
        "return student location if student is advisee of current advisor"
        certificates = advisorStudentGuard(self.env, self.certificates, studentName)
        if certificates:
            return StudentLocation(self.env, certificates)
        else:
            raise gateway.AccessDenied, "current advisor does not advise this student"

class CourseLocation(AuthorizedResource.Location):
    """
    Course location gives access to grades for students in this course.
    """
    def __init__(self, env, certificates):
        self.setContext(env, certificates)
    def getHere(self):
        "return list students for this course"
        result = []
        course = self.certificates["course"]
        for person in GLOBAL_INFO:
            personInfo = GLOBAL_INFO[person]
            courseGrades = personInfo["courseGrades"]
            if course in courseGrades:
                result.append(person)
        result.sort()
        return result
    def locate(self, studentName):
        "return grade location if student attends course"
        certificates = courseGradeGuard(self.env, self.certificates, studentName)
        if certificates:
            return GradeLocation(self.env, certificates)
        else:
            raise gateway.AccessDenied, "student is not in course -- cannot access grade"

class GradeLocation(AuthorizedResource.Location):
    """
    Grade location gives access to a grade for a student in a course.
    """
    def __init__(self, env, certificates):
        self.setContext(env, certificates)
    def getHere(self):
        "get grade for current student and course"
        studentName = self.certificates["student"]
        studentInfo = GLOBAL_INFO[studentName]
        courseGrades = studentInfo["courseGrades"]
        course = self.certificates["course"]
        return courseGrades[course]
    def putHere(self, value):
        "set grade for current student and course"
        # the request is authorized to modify the grade only if gradeWriteable is set.
        if not self.certificates.get("gradeWriteable"):
            raise gateway.AccessDenied, "user is not permitted to modify course grade"
        studentName = self.certificates["student"]
        studentInfo = GLOBAL_INFO[studentName]
        courseGrades = studentInfo["courseGrades"]
        course = self.certificates["course"]
        courseGrades[course] = value

